﻿#region Header
// ------------------------ Licence / Copyright ------------------------
// 
// ScrumTable for Scrum Meeting Support
// Copyright © HSR - Hochschule für Technik Rapperswil
// All Rights Reserved
// 
// Author:
//  Michael Gfeller Silvan Gehrig Patrick Boos
// 
// ---------------------------------------------------------------------
#endregion

#region Usings

using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using ScrumTable.Common;
using ScrumTable.Common.Collections;
using ScrumTable.Common.EventHandling;
using ScrumTable.DL.Data.Generic.Properties;

#endregion

namespace ScrumTable.DL.Data.Generic.Cache
{
    /// <summary>
    /// Represents the base class for all team foundation elements.
    /// </summary>
    public class CacheTrackedDictionary : DisposeBase, INotifyPropertyChanged, IEnumerable<ICacheTrackedDictionaryBucket>
    {
        #region Declarations
        //--------------------------------------------------------------------
        // Declarations
        //--------------------------------------------------------------------

        private readonly IDictionary<string, ICacheTrackedDictionaryBucket> _trackingProperties = new Dictionary<string, ICacheTrackedDictionaryBucket>();
        private readonly FastSmartWeakEvent<PropertyChangedEventHandler> _pptyChangedEvents = new FastSmartWeakEvent<PropertyChangedEventHandler>();
        private readonly FastSmartWeakEvent<PropertyChangingEventHandler> _pptyChangingEvents = new FastSmartWeakEvent<PropertyChangingEventHandler>();

        #endregion

        #region Properties
        //--------------------------------------------------------------------
        // Properties
        //--------------------------------------------------------------------

        /// <summary>
        /// Returns true, if a bucket element of this instance is dirty.
        /// </summary>
        public bool IsDirty
        {
            get
            {
                foreach (KeyValuePair<string, ICacheTrackedDictionaryBucket> bucketPair in _trackingProperties)
                {
                    if (bucketPair.Value.IsDirty)
                        return true;
                }
                return false;
            }
        }

        /// <summary>
        /// Gets the buckets which contain changes.
        /// </summary>
        internal IEnumerable<ICacheTrackedDictionaryBucket> ChangedBuckets
        {
            get { return (from bucket in _trackingProperties where bucket.Value.IsDirty select bucket.Value); }
        }

        #endregion

        #region Constructors / Destructor
        //--------------------------------------------------------------------
        // Constructors / Destructor
        //--------------------------------------------------------------------

        /// <summary>
        /// Creates a new CacheTrackedDictionary instance.
        /// </summary>
        internal CacheTrackedDictionary()
        {
        }

        #endregion

        #region Methods
        //--------------------------------------------------------------------
        // Methods
        //--------------------------------------------------------------------

        /// <summary>
        /// Returns an enumerator that iterates through a collection.
        /// </summary>
        /// <returns>
        /// An <see cref="T:System.Collections.IEnumerator"/> object that can be used to iterate through the collection.
        /// </returns>
        /// <filterpriority>2</filterpriority>
        IEnumerator IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }

        /// <summary>
        /// Returns an enumerator that iterates through the collection.
        /// </summary>
        /// <returns>
        /// A <see cref="T:System.Collections.Generic.IEnumerator`1"/> that can be used to iterate through the collection.
        /// </returns>
        /// <filterpriority>1</filterpriority>
        public IEnumerator<ICacheTrackedDictionaryBucket> GetEnumerator()
        {
            return _trackingProperties.Values.GetEnumerator();
        }

        /// <summary>
        /// Removes the dirty flag. This should be done after a clone has
        /// been created of the current element so the changes should be
        /// commited to the server.
        /// </summary>
        public void RemoveDirtyFlag()
        {
            foreach (KeyValuePair<string, ICacheTrackedDictionaryBucket> bucketPair in _trackingProperties)
            {
                bucketPair.Value.RemoveDirtyFlag();
            }
        }

        /// <summary>
        /// Returns true if the current instance contains a property with the
        /// specified name.
        /// </summary>
        /// <param name="propertyName">Specifies the name of the property to get.</param>
        public bool Contains(string propertyName)
        {
            return _trackingProperties.ContainsKey(propertyName);
        }

        /// <summary>
        /// Merges the values of the given object into the current cache
        /// element. The cache element must be of the same type as the
        /// current instance is.
        /// </summary>
        /// <param name="toMerge">Specifies the object to import.</param>
        /// <param name="saveProgressFeedback">Specifies the save feedback from the underlying system.</param>
        public virtual void MergeChanges(CacheTrackedDictionary toMerge, ICacheSaveProgressFeedback saveProgressFeedback)
        {
            PreCondition.AssertNotNull(toMerge, "toMerge");
            PreCondition.AssertNotNull(saveProgressFeedback, "saveProgressFeedback");
            PreCondition.AssertTrue(toMerge.GetType() == GetType(), Resources.ExcCacheElemBaseInvalidImportType);

            switch (saveProgressFeedback.State)
            {
                case CacheSavePairState.Saved:
                    OnMergeChangeSaved(toMerge, saveProgressFeedback);
                    break;
                case CacheSavePairState.Deleted:
                    OnMergeChangeDeleted(toMerge, saveProgressFeedback);
                    break;
                case CacheSavePairState.Updated:
                    OnMergeChangeInvalidateData(toMerge, saveProgressFeedback);
                    break;
                case CacheSavePairState.Error:
                    OnMergeChangeError(toMerge, saveProgressFeedback);
                    break;
                default: throw new ArgumentOutOfRangeException();
            }
        }

        /// <summary>
        /// Imports the values of another CacheTrackedDictionary.
        /// </summary>
        /// <returns>Returns the cloned object.</returns>
        /// <remarks>The import avoids value tracking.</remarks>
        public void ImportValues(CacheTrackedDictionary toImport)
        {
            if (toImport == null)
                return;

            foreach (KeyValuePair<string, ICacheTrackedDictionaryBucket> trackingPair in toImport._trackingProperties)
            {
                _trackingProperties[trackingPair.Key] = trackingPair.Value.Clone();
            }
        }

        /// <summary>
        /// Gets the associated tracked value or a default value if the
        /// tracked value does not exist.
        /// </summary>
        /// <param name="valueType">Specifies the excpected data type for the current value.</param>
        /// <param name="propertyName">Specifies the property name of the tracked value.</param>
        /// <returns>Returns the found value or the given default value.</returns>
        public object TryGetTrackedValue(Type valueType, string propertyName)
        {
            PreCondition.AssertNotNull(valueType, "valueType");

            ICacheTrackedDictionaryBucket bucket = GetBucket(propertyName);

            if (bucket.Value == null)
            {
                return null;
            }
            if (valueType.IsAssignableFrom(bucket.Value.GetType()))
            {
                return bucket.Value;
            }
            return bucket.Value.ChangeType(valueType);
        }

        /// <summary>
        /// Gets a tracked property value.
        /// </summary>
        /// <typeparam name="T">Contains the type of the value to change.</typeparam>
        /// <param name="propertyName">Specifies the name of the underlying property.</param>
        /// <returns>Returns the retrieved value.</returns>
        public T TryGetTrackedValue<T>(string propertyName)
        {
            object foundValue = TryGetTrackedValue(typeof (T), propertyName);
            
            if (foundValue == null)
            {
                return default(T);
            }
            return (T)foundValue;
        }

        /// <summary>
        /// Sets a value in the property store without tracking the change.
        /// </summary>
        /// <typeparam name="T">Contains the type of the value to change.</typeparam>
        /// <param name="propertyName">Specifies the name of the underlying property.</param>
        /// <param name="serverValue">Specifies the old (server side) value to set.</param>
        /// <param name="newValue">Specifies the value to set.</param>
        public void SetTrackedValue<T>(string propertyName, T serverValue, T newValue)
        {
            ICacheTrackedDictionaryBucket bucket = GetBucket(propertyName);
            bucket.OriginalValue = newValue;
            bucket.Value = newValue;
            bucket.LastBackEndValue = serverValue;
        }

        /// <summary>
        /// Tracks the change on the current element and executes the change
        /// action of the values that are not the same. This action will cause the
        /// changed flag to be set.
        /// </summary>
        /// <param name="propertyName">Specifies the name of the underlying property.</param>
        /// <param name="newValue">Specifies the new value to set.</param>
        /// <typeparam name="T">Contains the type of the value to change.</typeparam>
        /// <returns>Returns true if the value has been set and it's a new value, otherwise false.</returns>
        public bool TrackChange<T>(string propertyName, T newValue)
        {
            return TrackChange(propertyName, default(T), newValue);
        }

        /// <summary>
        /// Tracks the change on the current element and executes the change
        /// action of the values that are not the same. This action will cause the
        /// changed flag to be set.
        /// </summary>
        /// <param name="propertyName">Specifies the name of the underlying property.</param>
        /// <param name="newValue">Specifies the new value to set.</param>
        /// <param name="serverValue">Specifies the old (server side) value to set.</param>
        /// <typeparam name="T">Contains the type of the value to change.</typeparam>
        /// <returns>Returns true if the value has been set and it's a new value, otherwise false.</returns>
        public bool TrackChange<T>(string propertyName, T serverValue, T newValue)
        {
            bool isNew;
            ICacheTrackedDictionaryBucket bucket = GetBucket(propertyName, out isNew);

            if (isNew)
                bucket.OriginalValue = newValue;

            if (!Equals(bucket.Value, newValue))
            {
                bucket.Value = newValue;
                bucket.LastBackEndValue = serverValue;
            }
            return bucket.IsDirty;
        }

        /// <summary>
        /// Returns a read only dictionary of elements with the given type as dictionary value.
        /// </summary>
        /// <typeparam name="T">Specifies the dictionary type value.</typeparam>
        /// <returns>Returns the created dictionary.</returns>
        public IDictionary<string, ICollection<T>> ToDictionary<T>(Predicate<string> exclude = null)
        {
            IDictionary<string, ICollection<T>> values = new Dictionary<string, ICollection<T>>(_trackingProperties.Count);

            foreach (var valueProperty in _trackingProperties)
            {
                if (exclude == null || !exclude(valueProperty.Key))
                {
                    values[valueProperty.Key] = new[]
                                                    {
                                                        ObjectUtil.ChangeType<T>(valueProperty.Value.LastBackEndValue),
                                                        ObjectUtil.ChangeType<T>(valueProperty.Value.Value)
                                                    };
                }
            }
            return new ReadOnlyDictionary<string, ICollection<T>>(values);
        }

        /// <summary>
        /// Returns a read only dictionary of elements with the given type as dictionary value.
        /// </summary>
        /// <returns>Returns the created dictionary.</returns>
        public IDictionary<string, object> ToDictionary(Predicate<string> exclude = null)
        {
            IDictionary<string, object> values = new Dictionary<string, object>(_trackingProperties.Count);

            foreach (var valueProperty in _trackingProperties)
            {
                if (exclude == null || !exclude(valueProperty.Key))
                {
                    values[valueProperty.Key] = valueProperty.Value.Value;
                }
            }
            return new ReadOnlyDictionary<string, object>(values);
        }

        /// <summary>
        /// Gets the full history of the associated tracked value or null if the
        /// tracked value does not exist.
        /// </summary>
        /// <param name="key">Specifies the key of the tracked value.</param>
        /// <returns>Returns the found value or the given default value.</returns>
        internal ICacheTrackedDictionaryBucket TryGetTrackedBucket(string key)
        {
            if (key != null && Contains(key))
            {
                return GetBucket(key);
            }
            return null;
        }

        /// <summary>
        /// Releases managed (c# or .net) resources.
        /// </summary>
        protected override void ReleaseManagedResources()
        {
            base.ReleaseManagedResources();

            _trackingProperties.ForEach(namePropPair =>
                                            {
                                                namePropPair.Value.ValueChanged -= OnBucketValueChanged;
                                                namePropPair.Value.ValueChanging -= OnBucketValueChanging;
                                            });
        }


        private void ImportFailedChanges(CacheTrackedDictionary toMerge, ICacheSaveProgressFeedback saveProgressFeedback)
        {
            foreach (KeyValuePair<string, ICacheTrackedDictionaryBucket> trackingPairToImport in toMerge._trackingProperties)
            {
                ICacheTrackedDictionaryBucket bucket = GetBucket(trackingPairToImport.Key);

                /*
                 * if there were no change made during the save procedure
                 */
                if (bucket.CacheRevision == trackingPairToImport.Value.CacheRevision)
                {
                    bucket.MergeChanges(trackingPairToImport.Value, saveProgressFeedback);
                }
            }
        }

        private void ImportSavedChanges(CacheTrackedDictionary toMerge, ICacheSaveProgressFeedback saveProgressFeedback)
        {
            foreach (KeyValuePair<string, ICacheTrackedDictionaryBucket> trackingPairToImport in toMerge._trackingProperties)
            {
                GetBucket(trackingPairToImport.Key).MergeChanges(trackingPairToImport.Value, saveProgressFeedback);
            }

            foreach (string entryKey in _trackingProperties.Keys.ToArray())
            {
                if (!toMerge.Contains(entryKey))
                {
                    RemoveBucket(entryKey);
                }
            }
        }

        private ICacheTrackedDictionaryBucket GetBucket(string propertyName)
        {
            bool isNew;
            return GetBucket(propertyName, out isNew);
        }

        private ICacheTrackedDictionaryBucket GetBucket(string propertyName, out bool isNew)
        {
            isNew = false;
            if (!_trackingProperties.ContainsKey(propertyName))
            {
                _trackingProperties[propertyName] = new CacheTrackedDictionaryBucket { Name = propertyName };
                _trackingProperties[propertyName].ValueChanged += OnBucketValueChanged;
                _trackingProperties[propertyName].ValueChanging += OnBucketValueChanging;
                isNew = true;
            }
            return _trackingProperties[propertyName];
        }

        private void RemoveBucket(string propertyName)
        {
            if (_trackingProperties.ContainsKey(propertyName))
            {
                ICacheTrackedDictionaryBucket bucket = _trackingProperties[propertyName];
                _trackingProperties.Remove(propertyName);
                bucket.ValueChanged -= OnBucketValueChanged;
                bucket.ValueChanging -= OnBucketValueChanging;
            }
        }

        #endregion

        #region Events
        //--------------------------------------------------------------------
        // Events
        //--------------------------------------------------------------------

        /// <summary>
        /// Occurs when a property value changes.
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged
        {
            add { _pptyChangedEvents.Add(value); }
            remove { _pptyChangedEvents.Remove(value); }
        }

        /// <summary>
        /// Occurs before a property value changes.
        /// </summary>
        public event PropertyChangingEventHandler PropertyChanging
        {
            add { _pptyChangingEvents.Add(value); }
            remove { _pptyChangingEvents.Remove(value); }
        }

        /// <summary>
        /// Fires the PropertyChanged event.
        /// </summary>
        /// <param name="e">Specifies the event args to fire to the clients.</param>
        protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
        {
            _pptyChangedEvents.Raise(this, e);
        }

        /// <summary>
        /// Fires the PropertyChanging event.
        /// </summary>
        /// <param name="e">Specifies the event args to fire to the clients.</param>
        protected virtual void OnPropertyChanging(PropertyChangingEventArgs e)
        {
            _pptyChangingEvents.Raise(this, e);
        }

        private void OnBucketValueChanged(object sender, EventArgs e)
        {
            OnPropertyChanged(new PropertyChangedEventArgs(((ICacheTrackedDictionaryBucket)sender).Name));
        }

        private void OnBucketValueChanging(object sender, EventArgs e)
        {
            OnPropertyChanging(new PropertyChangingEventArgs(((ICacheTrackedDictionaryBucket)sender).Name));
        }


        /// <summary>
        /// An error occurred while saving the given cache element. This means, that
        /// the given element must be merged back (if there are no changes) into
        /// the cache tree.
        /// </summary>
        /// <param name="toMerge">Specifies the cache element which caused the error.</param>
        /// <param name="saveProgressFeedback">Specifies the save feedback provided by the data driver.</param>
        protected virtual void OnMergeChangeError(CacheTrackedDictionary toMerge, ICacheSaveProgressFeedback saveProgressFeedback)
        {
            ImportFailedChanges(toMerge, saveProgressFeedback);
        }

        /// <summary>
        /// An element on server side has changed (which behaviour may caused an error). Its
        /// cache data must be invalidated in order to resynchronize with the server.
        /// </summary>
        /// <param name="toMerge">Specifies the cache element which data should be invalidated.</param>
        /// <param name="saveProgressFeedback">Specifies the save feedback provided by the data driver.</param>
        protected virtual void OnMergeChangeInvalidateData(CacheTrackedDictionary toMerge, ICacheSaveProgressFeedback saveProgressFeedback)
        {
            ImportSavedChanges(toMerge, saveProgressFeedback);
        }

        /// <summary>
        /// An element has saved on server side. This means that data in the local storage may
        /// be invalidated if the change caused additional changes on server side. Workflow
        /// changes may cause additional value changes.
        /// </summary>
        /// <param name="toMerge">Specifies the cache element which data have ben saved.</param>
        /// <param name="saveProgressFeedback">Specifies the save feedback provided by the data driver.</param>
        protected virtual void OnMergeChangeSaved(CacheTrackedDictionary toMerge, ICacheSaveProgressFeedback saveProgressFeedback)
        {
            ImportSavedChanges(toMerge, saveProgressFeedback);
        }

        /// <summary>
        /// An element on server side has been deleted. The element in the cache must also be 
        /// deleted.
        /// </summary>
        /// <param name="toMerge">Specifies the cache element which has been deleted.</param>
        /// <param name="saveProgressFeedback">Specifies the save feedback provided by the data driver.</param>
        protected virtual void OnMergeChangeDeleted(CacheTrackedDictionary toMerge, ICacheSaveProgressFeedback saveProgressFeedback)
        {
        }

        #endregion
    }
}